/*
 * Copyright 2009 - sanatkumar.padhy
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *  
 *  	http://www.apache.org/licenses/LICENSE-2.0
 *  
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS, 
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and 
 *  limitations under the License. 
 */
package com.googlecode.simplecrud.dao;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import com.googlecode.simplecrud.DatabaseException;
import com.googlecode.simplecrud.SimpleCrudException;
import com.googlecode.simplecrud.annotations.Column;
import com.googlecode.simplecrud.annotations.Entity;
import com.googlecode.simplecrud.annotations.TypeConverter;
import com.googlecode.simplecrud.dao.util.DataTypeConverter;

/**
 * This implements {@link SimpleDao} to provide the actual implementation. A
 * spring wired singleton can be created by injecting the dataSource for
 * efficient use.
 * 
 * @param <K>
 *            - the key
 * @param <D>
 *            - the entity object
 * @author sanatkumar.padhy
 */
@SuppressWarnings("unchecked")
public class SimpleDaoImpl<K, D> extends DaoSupport implements SimpleDao<K, D>
{
	/**
	 * @see net.java.dev.simplecrud.dao.SimpleDao#delete(java.lang.Object)
	 */
	@Override
	public void delete(K key)
	{
		String column = null;
		StringBuffer query = new StringBuffer();

		String table = findTableName(key);
		Map<String, Object> keyValueMap = createKeyValueMap(key);

		query.append("delete from ").append(table);

		if (!keyValueMap.keySet().isEmpty()) {
			query.append(" where ");
		}

		List<Object> values = new ArrayList<Object>();
		for (Iterator<String> keyIterator = keyValueMap.keySet().iterator(); keyIterator.hasNext();) {
			column = keyIterator.next();
			query.append(column).append("=?");
			values.add(keyValueMap.get(column));

			if (keyIterator.hasNext()) {
				query.append(" and ");
			}
		}

		updateDatabase(query.toString(), values);
	}

	/**
	 * @see net.java.dev.simplecrud.dao.SimpleDao#insert(java.lang.Object)
	 */
	@Override
	public void insert(D dataObject)
	{
		StringBuffer query = new StringBuffer();
		StringBuffer columns = new StringBuffer();
		StringBuffer values = new StringBuffer();
		boolean firstLoop = true;

		String table = findTableName(dataObject);
		Map<String, Object> columnMap = createColumnValueMap(dataObject);

		query.append("insert into " + table + " (");

		List<Object> values1 = new ArrayList<Object>();
		for (String column : columnMap.keySet()) {
			if (!firstLoop) {
				columns.append(", ");
				values.append(", ");
			}
			columns.append(column);
			values.append("?");
			values1.add(columnMap.get(column));
			firstLoop = false;
		}

		query.append(columns.toString());
		query.append(") values (");
		query.append(values.toString());
		query.append(")");

		updateDatabase(query.toString(), values1);
	}

	/**
	 * @see net.java.dev.simplecrud.dao.SimpleDao#update(java.lang.Object)
	 */
	@Override
	public void update(D dataObject)
	{
		StringBuffer query = new StringBuffer();
		boolean firstLoop = true;
		String key = null;
		List<Object> values = new ArrayList<Object>();

		String table = findTableName(dataObject);
		Map<String, Object> columnMap = createColumnValueMap(dataObject);
		Map<String, Object> keyValueMap = createKeyValueMap(dataObject);

		query.append("update ").append(table).append(" set ");

		for (String column : columnMap.keySet()) {
			if (!firstLoop) {
				query.append(", ");
			}

			values.add(columnMap.get(column));
			query.append(column).append("=?");
			firstLoop = false;
		}

		// WHERE Clause
		if (!keyValueMap.keySet().isEmpty()) {
			query.append(" where ");
		}

		for (Iterator<String> keyIterator = keyValueMap.keySet().iterator(); keyIterator.hasNext();) {
			key = keyIterator.next();
			query.append(key).append("=?");
			values.add(keyValueMap.get(key));

			if (keyIterator.hasNext()) {
				query.append(" and ");
			}
		}

		updateDatabase(query.toString(), values);
	}

	/**
	 * @see net.java.dev.simplecrud.dao.SimpleDao#exists(java.lang.Object)
	 */
	@Override
	public boolean exists(K key)
	{
		Map<String, String> columnFieldMap = new HashMap<String, String>();
		ResultSet rs = fetchResultSet(key, columnFieldMap);

		try {
			if ((rs == null) || !rs.next()) { return false; }
		}
		catch (SQLException e) {
			// No record exist so we return false
			return false;
		}
		return true;
	}

	/**
	 * @see net.java.dev.simplecrud.dao.SimpleDao#retrieve(java.lang.Object)
	 */
	@Override
	public D retrieve(K key)
	{
		D data = null;
		Map<String, String> columnFieldMap = new HashMap<String, String>();
		Class entityObject = findEntityObject(key);

		ResultSet rs = fetchResultSet(key, columnFieldMap);

		try {
			if ((rs == null) || !rs.next()) // advance pointer
			{ return null; }
			data = (D) entityObject.getConstructor().newInstance();
			populateObject(rs, data, columnFieldMap);
		}
		catch (SQLException e) {
			throw new DatabaseException("Database Exception ::", e);
		}
		catch (Exception e) {
			throw new SimpleCrudException("Reflection Error :: ", e);
		}
		return data;
	}

	/**
	 * @see net.java.dev.simplecrud.dao.SimpleDao#retrieveList(java.lang.Object)
	 */
	@Override
	public List<D> retrieveList(K key)
	{
		D data = null;
		Map<String, String> columnFieldMap = new HashMap<String, String>();
		List<D> objectList = new ArrayList<D>();

		Class entityObject = findEntityObject(key);

		ResultSet rs = fetchResultSet(key, columnFieldMap);

		try {
			while (rs.next()) {
				data = (D) entityObject.getConstructor().newInstance();
				populateObject(rs, data, columnFieldMap);
				objectList.add(data);
			}
		}
		catch (SQLException e) {
			throw new DatabaseException("Database Exception ::", e);
		}
		catch (Exception e) {
			throw new SimpleCrudException("Reflection Error :: ", e);
		}

		return objectList;
	}

	/* Helper method for retrieve and retrieveList to fetch the ResultSet */
	private ResultSet fetchResultSet(K key, Map<String, String> columnFieldMap)
	{
		StringBuffer query = new StringBuffer();
		Map<String, Object> keyMap = new HashMap<String, Object>();
		Class entityObject = findEntityObject(key);
		List<Object> values = new ArrayList<Object>();

		createMetaData(key, keyMap, columnFieldMap, entityObject);

		// Create the query
		query.append("select ");
		// add all the columns that need to be retrieved
		for (Iterator<String> columnIterator = columnFieldMap.keySet().iterator(); columnIterator.hasNext();) {
			query.append(columnFieldMap.get(columnIterator.next()));
			if (columnIterator.hasNext()) {
				query.append(", ");
			}
		}

		query.append(" from " + findTableName(key));
		String key1 = null;
		if (!keyMap.keySet().isEmpty()) {
			query.append(" where ");
			for (Iterator<String> keyIterator = keyMap.keySet().iterator(); keyIterator.hasNext();) {
				key1 = keyIterator.next();
				query.append(key1).append("=?");
				values.add(keyMap.get(key1));
				if (keyIterator.hasNext()) {
					query.append(", ");
				}
			}
		}

		// Hit Database
		return getResult(query.toString(), values);
	}

	/*
	 * Find the persistent class from the key object, This method assumes the
	 * key is annotated with @Entity
	 */
	private Class findEntityObject(K key)
	{
		Entity entityObject = key.getClass().getAnnotation(Entity.class);
		if (entityObject == null) { throw new SimpleCrudException("No Entity Object provided for the Key" + key.getClass()); }
		Class clazz = null;

		try {
			clazz = Class.forName(entityObject.value());
		}
		catch (ClassNotFoundException e1) {
			throw new SimpleCrudException("No Such entity object found: " + entityObject.value());
		}
		return clazz;
	}

	/**
	 * Utility method to create different meta-data required for retrieve
	 * operation.
	 * 
	 * @param keyObject
	 *            - the entity key
	 * @param keyMap
	 *            - holds the values for the keys
	 * @param columnFieldMap
	 *            - creates a complete column - field mapping using the entity
	 *            object
	 * @param entityObject
	 *            - the entity object
	 */
	protected void createMetaData(K keyObject, Map<String, Object> keyMap, Map<String, String> columnFieldMap, Class entityObject)
	{
		Annotation fieldAnn;
		String column;

		Field[] fields = entityObject.getDeclaredFields();

		for (Field field : fields) {
			field.setAccessible(true);
			fieldAnn = field.getAnnotation(Column.class);
			if (fieldAnn != null) {
				column = ((Column) fieldAnn).name();
				columnFieldMap.put(field.getName(), column);
			}
		}

		Field[] keyFields = keyObject.getClass().getDeclaredFields();
		for (Field field2 : keyFields) {
			field2.setAccessible(true);
			fieldAnn = field2.getAnnotation(Column.class);
			if (fieldAnn != null) {
				column = ((Column) fieldAnn).name();
				try {
					Object value = field2.get(keyObject);
					if (value != null) {
						keyMap.put(column, value);
					}
				}
				catch (IllegalArgumentException e) {
					throw new SimpleCrudException("Illegal argument for field:" + field2.getName() + " in "
							+ keyObject.getClass().getSimpleName());
				}
				catch (IllegalAccessException e) {
					throw new SimpleCrudException("Illegal Access to field: " + field2.getName());
				}
			}
		}
	}

	/**
	 * This method is used to populate the persistence object with the resultSet
	 * using <code>columnFieldMapping</code>. This also used the type converter
	 * specified using <code>@TypeConverter</code>
	 * 
	 * @param crs
	 * @param dataObject
	 * @param columnFieldMap
	 */
	protected void populateObject(ResultSet crs, D dataObject, Map<String, String> columnFieldMap)
	{
		Annotation fieldAnn, converterAnn;
		Class clazz;
		String columnName = null;
		DataTypeConverter converter = null;
		Field[] fields = dataObject.getClass().getDeclaredFields();

		for (Field field : fields) {
			converter = null;
			field.setAccessible(true);
			fieldAnn = field.getAnnotation(Column.class);
			if (fieldAnn != null) {
				columnName = columnFieldMap.get(field.getName());
				converterAnn = field.getAnnotation(TypeConverter.class);
				if (converterAnn != null) {
					// get instance of converter
					try {
						clazz = Class.forName(((TypeConverter) converterAnn).type());
						converter = (DataTypeConverter) clazz.getConstructor().newInstance();
					}
					catch (Exception e) {
						throw new SimpleCrudException("Error initializing DataTypeConverter :: ", e);
					}
				}
				try {
					if (converter != null) {
						field.set(dataObject, converter.convertFromDbValue(crs.getObject(columnName)));
					}
					else {
						field.set(dataObject, crs.getObject(columnName));
					}

				}
				catch (IllegalArgumentException e) {
					throw new SimpleCrudException("Illegal argument for field:" + field.getName() + " in "
							+ dataObject.getClass().getSimpleName(), e);
				}
				catch (IllegalAccessException e) {
					throw new SimpleCrudException("Illegal Access to field: " + field.getName(), e);
				}
				catch (SQLException e) {
					throw new DatabaseException("Error populating data object for " + columnName, e);
				}
			}
		}
	}
}
